window.addEventListener("DOMContentLoaded",app);

function app() {
	let asideBtn = document.getElementById("aside-btn"),
		resetBtn = document.getElementById("reset-btn"),
		imgUpload = document.getElementsByName("img_upload")[0],
		imgName = document.getElementsByName("img_name")[0],
		canvas = document.createElement("canvas"),
		c = canvas.getContext("2d"),
		img = null,
		scene,
		camera,
		camControls,
		renderer,
		textureLoader = new THREE.TextureLoader(),
		city,
		dust,
		// adjustable
		skyColor = 0x8fa6af,
		terrainColor = 0xe8bfa9,
		chunkSize = 64,
		gridSize = 7,
		roadWidth = 8,
		minBldgHt = 16,
		maxBldgHt = 48,
		bldgSize = 12,
		bldgFragHt = 2,
		bldgsPerChunkSide = 3,
		bldgDisplaceFactor = 0.25,
		dustParticleSpeed = 0.2,
		dustParticlesPerChunk = 12,
		sunAngle = 30,
		worldHeight = 64,
		worldSize = 1600,
		// technical
		chunkSizeHalf = chunkSize / 2,
		gridSizeEven = gridSize % 2 == 0,
		gridSizeHalf = gridSize / 2,
		bldgCellsPerSide = bldgsPerChunkSide * gridSize,
		roadsSide = chunkSize * gridSize + roadWidth,
		roadsSideHalf = roadsSide / 2,
		dustParticles = dustParticlesPerChunk * gridSize ** 2,
		// functions
		adjustWindow = () => {
			camera.aspect = window.innerWidth / window.innerHeight;
			camera.updateProjectionMatrix();
			renderer.setSize(window.innerWidth,window.innerHeight);
		},
		clearCity = () => {
			img = null;

			if (scene) {
				let children = city.children;

				while (children.length) {
					let child = children[0];
					// kill windows of a building
					if (child.name == "Civilian Structure") {
						let gchild = child.children[0];
						gchild.geometry.dispose();
						gchild.material.dispose();
						child.remove(gchild);
					}
					child.geometry.dispose();
					child.material.dispose();
					city.remove(child);
				}
			}
		},
		getDistance = (x1,y1,x2,y2) => {
			let raw = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2),
				rounded = Math.round(raw * 1e3) / 1e3;

			return rounded;
		},
		getLightness = (R,G,B) => {
			let r = R / 255,
				g = G / 255,
				b = B / 255,
				cmin = Math.min(r,g,b),
				cmax = Math.max(r,g,b),
				light = (cmax + cmin) / 2;

			return light;
		},
		generateCity = imgData => {
			let bldgSizeHalf = bldgSize / 2,
				bldgNegEdge = -bldgSizeHalf - 0.01,
				bldgPosEdge = bldgSizeHalf + 0.01,
				chunkDiv = chunkSize / bldgsPerChunkSide,
				chunkDivHalf = chunkDiv / 2,
				bldgTrans = -(chunkSize * gridSizeHalf) + chunkDivHalf,
				pixelIndex = 0,
				sidewalks = scene.children.filter(child => child.name == "Sidewalk");

			for (let z = 0; z < bldgCellsPerSide; ++z) {
				for (let x = 0; x < bldgCellsPerSide; ++x) {
					if (
						bldgsPerChunkSide % 2 == 0 ||
						z % bldgsPerChunkSide != Math.floor(bldgsPerChunkSide / 2) ||
						x % bldgsPerChunkSide != Math.floor(bldgsPerChunkSide / 2)
					) {
						// building
						let bldgHeight,
							bldgHeightHalf,
							bldgGeo,
							bldgMat,
							bldg,
							alpha = imgData ? imgData[pixelIndex + 3] : 0;

						if (imgData && alpha > 0.1) {
							let red = imgData[pixelIndex],
								green = imgData[pixelIndex + 1],
								blue = imgData[pixelIndex + 2],
								lightness = getLightness(red,green,blue);

							bldgHeight = minBldgHt + Math.round(lightness * (maxBldgHt - minBldgHt) / bldgFragHt) * bldgFragHt;
							bldgHeightHalf = bldgHeight / 2;
							bldgGeo = new THREE.BoxBufferGeometry(bldgSize,bldgHeight,bldgSize);
							bldgMat = new THREE.MeshPhongMaterial({
								color: `rgb(${red},${green},${blue})`
							});
							bldg = new THREE.Mesh(bldgGeo,bldgMat);

						} else {
							let randHue = randomHue();
							bldgHeight = minBldgHt + Math.round(Math.random() * (maxBldgHt - minBldgHt) / bldgFragHt) * bldgFragHt;
							bldgHeightHalf = bldgHeight / 2;
							bldgGeo = new THREE.BoxBufferGeometry(bldgSize,bldgHeight,bldgSize);
							bldgMat = new THREE.MeshPhongMaterial({
								color: `hsl(${randHue},50%,45%)`
							});
							bldg = new THREE.Mesh(bldgGeo,bldgMat);
						}

						bldgMat.shininess = 90;
						bldg.name = "Civilian Structure";
						bldg.castShadow = true;
						bldg.receiveShadow = true;
						bldg.position.set(
							x * chunkDiv + bldgTrans,
							bldgHeight / 2 + 1,
							z * chunkDiv + bldgTrans
						);
						// displacement towards the center of the sidewalk
						let sidewalkIndex = Math.floor(x / bldgsPerChunkSide) + (gridSize * Math.floor(z / bldgsPerChunkSide)),
							sidewalk = sidewalks[sidewalkIndex];
						if (sidewalk) {
							let sidewalkCoords = sidewalks[sidewalkIndex].position,
								sX = sidewalkCoords.x,
								bX = bldg.position.x,
								sZ = sidewalkCoords.z,
								bZ = bldg.position.z,
								distFromSWCenter = getDistance(sX,sZ,bX,bZ),
								newDist = distFromSWCenter * (1 - bldgDisplaceFactor),
								distX = Math.abs(sX - bX),
								distZ = Math.abs(sZ - bZ),
								distAngle = Math.atan(distZ / distX);

							if (bX > sX && bZ <= sZ)
								distAngle += Math.PI / 2;
							else if (bX <= sX && bZ <= sZ)
								distAngle += Math.PI;
							else if (bX <= sX && bZ > sZ)
								distAngle += Math.PI * 1.5;

							bldg.position.x = sX + (newDist * Math.sin(distAngle));
							bldg.position.z = sZ + (newDist * Math.cos(distAngle));
						}

						city.add(bldg);
						// windows
						let windowGeo = new THREE.BufferGeometry(),
							windowVertArr = [];

						for (let wy = 0; wy < bldgHeight; wy += 2) {
							for (let wx = 0; wx < 12; wx += 2) {
								let leftWinEdge = (-bldgSizeHalf + 0.5) + wx,
									rightWinEdge = (-bldgSizeHalf + 1.5) + wx,
									bottomWinEdge = (-bldgHeightHalf + 0.5) + wy,
									topWinEdge = (-bldgHeightHalf + 1.5) + wy;

								windowVertArr.push(
									// north
									rightWinEdge,bottomWinEdge,bldgNegEdge,
									leftWinEdge,bottomWinEdge,bldgNegEdge,
									leftWinEdge,topWinEdge,bldgNegEdge,
									leftWinEdge,topWinEdge,bldgNegEdge,
									rightWinEdge,topWinEdge,bldgNegEdge,
									rightWinEdge,bottomWinEdge,bldgNegEdge,
									// east
									bldgPosEdge,bottomWinEdge,rightWinEdge,
									bldgPosEdge,bottomWinEdge,leftWinEdge,
									bldgPosEdge,topWinEdge,leftWinEdge,
									bldgPosEdge,topWinEdge,leftWinEdge,
									bldgPosEdge,topWinEdge,rightWinEdge,
									bldgPosEdge,bottomWinEdge,rightWinEdge,
									// south
									leftWinEdge,bottomWinEdge,bldgPosEdge,
									rightWinEdge,bottomWinEdge,bldgPosEdge,
									rightWinEdge,topWinEdge,bldgPosEdge,
									rightWinEdge,topWinEdge,bldgPosEdge,
									leftWinEdge,topWinEdge,bldgPosEdge,
									leftWinEdge,bottomWinEdge,bldgPosEdge,
									// west
									bldgNegEdge,bottomWinEdge,leftWinEdge,
									bldgNegEdge,bottomWinEdge,rightWinEdge,
									bldgNegEdge,topWinEdge,rightWinEdge,
									bldgNegEdge,topWinEdge,rightWinEdge,
									bldgNegEdge,topWinEdge,leftWinEdge,
									bldgNegEdge,bottomWinEdge,leftWinEdge
								);
							}
						}

						let windowVerts = new Float32Array(windowVertArr),
							windowMat = new THREE.MeshBasicMaterial({
								color: 0x17181c
							});
							windows = new THREE.Mesh(windowGeo,windowMat);

						windowGeo.setAttribute("position",new THREE.BufferAttribute(windowVerts,3));
						bldg.add(windows);
					}
					pixelIndex += 4;
				}
			}
		},
		handleImgUpload = e => {
			return new Promise((resolve,reject) => {
				if (imgUpload) {
					let target = !e ? imgUpload : e.target;
					if (target.files.length) {
						let reader = new FileReader();
						reader.onload = e2 => {
							img = new Image();
							img.src = e2.target.result;
							img.onload = () => {
								resolve();
							};
							img.onerror = () => {
								img = null;
								reject("The image was nullified due to corruption or a non-image upload.");
							};

							if (imgName)
								imgName.placeholder = target.files[0].name;
						};
						reader.readAsDataURL(target.files[0]);
					}

				} else {
					reject("The file input is missing.");
				}
			});
		},
		imgUploadValid = () => {
			if (imgUpload) {
				let files = imgUpload.files,
					fileIsThere = files.length > 0,
					isImage = files[0].type.match("image.*"),
					valid = fileIsThere && isImage;

				return valid;

			} else {
				return false;
			}
		},
		init = () => {
			// setup
			scene = new THREE.Scene();
			scene.fog = new THREE.Fog(skyColor,512,640);
			// renderer
			renderer = new THREE.WebGLRenderer({
				logarithmicDepthBuffer: true
			});
			renderer.setClearColor(skyColor);
			renderer.setSize(window.innerWidth,window.innerHeight);
			renderer.shadowMap.enabled = true;
			// camera
			camera = new THREE.PerspectiveCamera(60,window.innerWidth / window.innerHeight,0.1,1000);
			camera.position.set(160,160,160);
			camera.lookAt(scene.position);
			camControls = new THREE.OrbitControls(camera,renderer.domElement);
			camControls.enablePan = false;
			camControls.minDistance = 8;
			camControls.maxDistance = 512;
			camControls.minPolarAngle = -Math.PI / 2;
			camControls.maxPolarAngle = Math.PI / 2;
			// lighting
			let daylight = new THREE.AmbientLight(0xfbfbb6,1);
			daylight.name = "Daylight";
			scene.add(daylight);

			let sun = new THREE.PointLight(0xffffff,2,worldSize,2);
			sun.name = "Sun";
			sun.position.set(
				worldSize / 2 * Math.sin(sunAngle * Math.PI / 180),
				worldSize / 2 * Math.cos(sunAngle * Math.PI / 180),
				0
			);
			sun.castShadow = true;
			scene.add(sun);
			// terrain
			let terrainGeo = new THREE.PlaneBufferGeometry(worldSize,worldSize),
				terrainMat = new THREE.MeshStandardMaterial({
					color: terrainColor
				}),
				terrain = new THREE.Mesh(terrainGeo,terrainMat);
			terrain.name = "Terrain";
			terrain.rotation.x = -0.5 * Math.PI;
			terrain.position.y = -0.01;
			terrain.receiveShadow = true;
			scene.add(terrain);
			// roads
			let roadGeo = new THREE.PlaneBufferGeometry(roadsSide,roadsSide),
				roadMat = new THREE.MeshPhongMaterial({
					color: 0x2e3138
				}),
				road = new THREE.Mesh(roadGeo,roadMat);
			roadMat.shininess = 35;
			road.name = "Road";
			road.rotation.x = -0.5 * Math.PI;
			road.receiveShadow = true;
			scene.add(road);
			// sidewalks
			let sidewalkGeo = new THREE.BoxBufferGeometry(
					chunkSize - roadWidth,
					1,
					chunkSize - roadWidth
				),
				sidewalkMat = new THREE.MeshPhongMaterial({
					color: 0x5c6270
				}),
				sidewalk = new THREE.Mesh(sidewalkGeo,sidewalkMat);

			sidewalk.name = "Sidewalk";
			sidewalk.receiveShadow = true;

			let zStart = -Math.floor(gridSizeHalf),
				zEnd = -zStart - (gridSizeEven ? 1 : 0),
				xStart = zStart,
				xEnd = zEnd;

			for (let z = zStart; z <= zEnd; ++z) {
				for (let x = xStart; x <= xEnd; ++x) {
					let sidewalkUnit = sidewalk.clone();
					sidewalkUnit.position.set(
						chunkSize * x,
						0.5,
						chunkSize * z
					);
					if (gridSizeEven) {
						sidewalkUnit.position.x += chunkSizeHalf;
						sidewalkUnit.position.z += chunkSizeHalf;
					}
					scene.add(sidewalkUnit);
				}
			}
			// build the city
			city = new THREE.Object3D();
			city.name = "City";
			scene.add(city);
			generateCity();
			// dust particles
			let dustGeo = new THREE.BufferGeometry(),
				dustVertArr = [];

			for (let p = 0; p < dustParticles; ++p) {
				dustVertArr.push(
					Math.round(roadsSide * Math.random() - roadsSide / 2),
					Math.round(Math.random() * worldHeight),
					Math.round(roadsSide * Math.random() - roadsSide / 2)
				);
			}
			let dustVerts = new Float32Array(dustVertArr),
				dustMat = new THREE.PointsMaterial({
					map: textureLoader.load("https://i.ibb.co/mqQrvZ1/dust.png"),
					color: 0xffff00,
					size: 2,
					transparent: true
				});

			dustGeo.setAttribute("position",new THREE.BufferAttribute(dustVerts,3));
			dust = new THREE.Points(dustGeo,dustMat);
			dust.name = "Dust Particles";
			scene.add(dust);
			// render
			let body = document.body;
			body.insertBefore(renderer.domElement,body.firstChild);
			renderScene();
			// deal with preserved input
			if (imgUpload && imgUpload.value != "")
				renderPromise();
		},
		moveDust = () => {
			let posArr = dust.geometry.attributes.position.array,
				dirs = 8,
				newPosArr = posArr.map((a,i) => {
					let dim = i % 3,
						dir = i % dirs,
						angle = 360 * (dir / dirs) * Math.PI / 180;

					if (dim == 0)
						a += dustParticleSpeed * Math.sin(angle);
					else if (dim == 2)
						a += dustParticleSpeed * Math.cos(angle);

					if (dim == 0 || dim == 2) {
						a += dustParticleSpeed;
						if (a > roadsSideHalf)
							a -= roadsSide;
						else if (a < -roadsSideHalf)
							a += roadsSide;

					} else if (dim == 1) {
						a += dustParticleSpeed;
						if (a >= worldHeight)
							a = 0;
					}

					return a;
				});

			let newDustVerts = new Float32Array(newPosArr);
			dust.geometry.setAttribute("position",new THREE.BufferAttribute(newDustVerts,3));
			dust.geometry.verticesNeedUpdate = true;
		},
		randomHue = () => {
			let roundHueTo = 30,
				r = Math.floor(Math.random() * 360 / roundHueTo) * roundHueTo;
			return r;
		},
		renderPromise = e => {
			handleImgUpload(e).then(() => {
				if (imgUploadValid()) {
					updateCanvas();
					updateImg();
				}

			}).catch(msg => {
				console.log(msg);
			});
		},
		renderScene = () => {
			moveDust();
			renderer.render(scene,camera);
			requestAnimationFrame(renderScene);
		},
		resetCity = () => {
			if (imgName)
				imgName.placeholder = "No file selected";

			clearCity();
			generateCity();
		},
		toggleAside = e => {
			let aside = document.querySelector("aside");
			if (aside) {
				let openClass = "aside-open";
				if (e.keyCode == 27)
					aside.classList.remove(openClass);
				else if (!e.keyCode)
					aside.classList.toggle(openClass);
			}
		},
		updateCanvas = () => {
			// restrict image size, keep it proportional
			let imgWidth = img.width,
				imgHeight = img.height;

			if (imgWidth >= imgHeight) {
				if (imgWidth >= bldgCellsPerSide) {
					imgWidth = bldgCellsPerSide;
					imgHeight = imgWidth * (img.height / img.width);
				}
			} else {
				if (imgHeight >= bldgCellsPerSide) {
					imgHeight = bldgCellsPerSide;
					imgWidth = imgHeight * (img.width / img.height);
				}
			}
			// update canvas
			c.clearRect(0,0,bldgCellsPerSide,bldgCellsPerSide);

			let imgX = bldgCellsPerSide / 2 - imgWidth / 2,
				imgY = bldgCellsPerSide / 2 - imgHeight / 2;

			c.drawImage(img,imgX,imgY,imgWidth,imgHeight);
		},
		updateImg = () => {
			let imgData = c.getImageData(0,0,bldgCellsPerSide,bldgCellsPerSide),
				data = imgData.data;

			clearCity();
			generateCity(data);
		};

	init();

	if (asideBtn) {
		asideBtn.addEventListener("click",toggleAside);
		window.addEventListener("keydown",toggleAside);
	}
	if (resetBtn)
		resetBtn.addEventListener("click",resetCity);
	if (imgUpload)
		imgUpload.addEventListener("change",renderPromise);

	window.addEventListener("resize",adjustWindow);
}
